﻿/*
 * Copy from Dapper: https://github.com/StackExchange/Dapper
 * License: http://www.apache.org/licenses/LICENSE-2.0
 * Home page: https://github.com/StackExchange/dapper-dot-net
 */

namespace Chloe.Query.Internals
{
    sealed partial class DapperRow
      : System.Dynamic.IDynamicMetaObjectProvider
      , IDictionary<string, object>
    {
        readonly DapperTable table;
        object[] values;

        public DapperRow(DapperTable table, object[] values)
        {
            if (table == null) throw new ArgumentNullException("table");
            if (values == null) throw new ArgumentNullException("values");
            this.table = table;
            this.values = values;
        }
        private sealed class DeadValue
        {
            public static readonly DeadValue Default = new DeadValue();
            private DeadValue() { }
        }
        int ICollection<KeyValuePair<string, object>>.Count
        {
            get
            {
                int count = 0;
                for (int i = 0; i < values.Length; i++)
                {
                    if (!(values[i] is DeadValue)) count++;
                }
                return count;
            }
        }

        public bool TryGetValue(string name, out object value)
        {
            var index = table.IndexOfName(name);
            if (index < 0)
            { // doesn't exist
                value = null;
                return false;
            }
            // exists, **even if** we don't have a value; consider table rows heterogeneous
            value = index < values.Length ? values[index] : null;
            if (value is DeadValue)
            { // pretend it isn't here
                value = null;
                return false;
            }
            return true;
        }

        public override string ToString()
        {
            var sb = new StringBuilder("{");
            string c = "";
            foreach (var kv in this)
            {
                var value = kv.Value;
                sb.Append(c);
                c = ", ";
                sb.Append(kv.Key);
                if (value != null)
                {
                    Type valueType = value.GetType();
                    if (valueType == typeof(string) || valueType == typeof(DateTime))
                        sb.Append(" = '").Append(kv.Value).Append('\'');
                    else
                        sb.Append(" = ").Append(kv.Value);
                }
                else
                {
                    sb.Append(" = null");
                }
            }

            return sb.Append('}').ToString();
        }

        System.Dynamic.DynamicMetaObject System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject(
            System.Linq.Expressions.Expression parameter)
        {
            return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this);
        }

        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            var names = table.FieldNames;
            for (var i = 0; i < names.Length; i++)
            {
                object value = i < values.Length ? values[i] : null;
                if (!(value is DeadValue))
                {
                    yield return new KeyValuePair<string, object>(names[i], value);
                }
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #region Implementation of ICollection<KeyValuePair<string,object>>

        void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
        {
            IDictionary<string, object> dic = this;
            dic.Add(item.Key, item.Value);
        }

        void ICollection<KeyValuePair<string, object>>.Clear()
        { // removes values for **this row**, but doesn't change the fundamental table
            for (int i = 0; i < values.Length; i++)
                values[i] = DeadValue.Default;
        }

        bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
        {
            object value;
            return TryGetValue(item.Key, out value) && Equals(value, item.Value);
        }

        void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
        {
            foreach (var kv in this)
            {
                array[arrayIndex++] = kv; // if they didn't leave enough space; not our fault
            }
        }

        bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
        {
            IDictionary<string, object> dic = this;
            return dic.Remove(item.Key);
        }

        bool ICollection<KeyValuePair<string, object>>.IsReadOnly
        {
            get { return false; }
        }

        #endregion

        #region Implementation of IDictionary<string,object>

        bool IDictionary<string, object>.ContainsKey(string key)
        {
            int index = table.IndexOfName(key);
            if (index < 0 || index >= values.Length || values[index] is DeadValue) return false;
            return true;
        }

        void IDictionary<string, object>.Add(string key, object value)
        {
            SetValue(key, value, true);
        }

        bool IDictionary<string, object>.Remove(string key)
        {
            int index = table.IndexOfName(key);
            if (index < 0 || index >= values.Length || values[index] is DeadValue) return false;
            values[index] = DeadValue.Default;
            return true;
        }

        object IDictionary<string, object>.this[string key]
        {
            get { object val; TryGetValue(key, out val); return val; }
            set { SetValue(key, value, false); }
        }

        public object SetValue(string key, object value)
        {
            return SetValue(key, value, false);
        }
        private object SetValue(string key, object value, bool isAdd)
        {
            if (key == null) throw new ArgumentNullException("key");
            int index = table.IndexOfName(key);
            if (index < 0)
            {
                index = table.AddField(key);
            }
            else if (isAdd && index < values.Length && !(values[index] is DeadValue))
            {
                // then semantically, this value already exists
                throw new ArgumentException("An item with the same key has already been added", "key");
            }
            int oldLength = values.Length;
            if (oldLength <= index)
            {
                // we'll assume they're doing lots of things, and
                // grow it to the full width of the table
                Array.Resize(ref values, table.FieldCount);
                for (int i = oldLength; i < values.Length; i++)
                {
                    values[i] = DeadValue.Default;
                }
            }
            return values[index] = value;
        }

        ICollection<string> IDictionary<string, object>.Keys
        {
            get { return this.Select(kv => kv.Key).ToArray(); }
        }

        ICollection<object> IDictionary<string, object>.Values
        {
            get { return this.Select(kv => kv.Value).ToArray(); }
        }

        #endregion
    }
}
